/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @file cookies.c
 * Cookies Management and parsing
 */

#include <time.h>

#include "cookies.h"
#include "logging.h"
#include "pyhp_layer.h"
#include <apr_strings.h>

#include "structmember.h"

#define PYHP_COOKIE_PREAMBLE "pyhp:"
#define PYHP_COOKIE_PREAMBLE_LEN 5

enum PyHPCookieType { PYHP_COOKIE_NORMAL, PYHP_COOKIE_SESSION };

typedef struct {
    PyObject_HEAD
    PyObject * name;
    PyObject * data;
    PyObject * attrs;
    int cookie_type;
} PyHPCookie;

/**
 * Python __setattr__ method for the Cookie class
 */
static PyObject * pyhp_cookie_set_attr(PyHPCookie * c, PyObject * args)
{
    PyObject * name, * value;
    if (!PyArg_ParseTuple(args, "OO", &name, &value))
    	return NULL;

    Py_INCREF(value);
    PyDict_SetItem(c->attrs, name, value);

    Py_INCREF(Py_None);
    return Py_None;
}

/**
 * Python __getattr__ method for the Cookie class
 */
static PyObject * pyhp_cookie_get_attr(PyHPCookie * c, PyObject * args)
{
    PyObject * name, * r;

    if (!PyArg_ParseTuple(args, "O", &name))
    	return NULL;

    r = PyDict_GetItem(c->attrs, name);

    if (!r)
        r = Py_None;

    Py_INCREF(r);
    return r;
}

/**
 * Python __str__ and __repr__ methods.
 * Converts a Cookie instance to a string to print it
 */
static PyObject *pyhp_cookie_str(PyHPCookie *self)
{
    PyObject * str_d = PyObject_CallMethod(self->data, "__str__", NULL);
    PyObject * str_a = PyObject_CallMethod(self->attrs, "__str__", NULL);

    PyObject * str = PyString_FromFormat("[%s : %s - %s]",
                                         PyString_AsString(self->name),
                                         PyString_AsString(str_d),
                                         PyString_AsString(str_a));

    Py_XDECREF(str_d);
    Py_XDECREF(str_a);

    return str;
}

static PyMethodDef PyHPCookie_methods[] = {
    {"set_attr", (PyCFunction)pyhp_cookie_set_attr, METH_VARARGS,
     PyDoc_STR("set a cookie attribute")},
    {"get_attr", (PyCFunction)pyhp_cookie_get_attr, METH_VARARGS,
     PyDoc_STR("get a cookie attribute")},
    {NULL,	NULL},
};

static PyMemberDef PyHPCookie_members[] = {
    {"data", T_OBJECT_EX, offsetof(PyHPCookie, data), 0, "Cookie Content"},
    {NULL}  /* Sentinel */
};

/**
 * Python Cookie constructor
 */
static PyHPCookie * PyHPCookie_new(PyTypeObject * type, PyObject * args, PyObject * kws)
{
    PyHPCookie * ck = (PyHPCookie*)type->tp_alloc(type, 0);
    if (ck) {
        ck->data = PyDict_New();
        ck->attrs = PyDict_New();
        ck->name = PyString_FromString("empty_name");
        ck->cookie_type = PYHP_COOKIE_NORMAL;
    }
    return ck;
}

/**
 * Python Cookie destructor
 */
static void PyHPCookie_dealloc(PyHPCookie * ck)
{
    Py_XDECREF(ck->data);
    Py_XDECREF(ck->attrs);
    ck->ob_type->tp_free((PyObject*)ck);
}


static PyTypeObject PyHPCookieType = {
    PyObject_HEAD_INIT(NULL)
    0,                       /* ob_size */
    "pyhp.cookies.Cookie",         /* tp_name */
    sizeof(PyHPCookie),          /* tp_basicsize */
    0,                       /* tp_itemsize */
    (destructor)PyHPCookie_dealloc,                       /* tp_dealloc */
    0,                       /* tp_print */
    0,                       /* tp_getattr */
    0,                       /* tp_setattr */
    0,                       /* tp_compare */
    (reprfunc)pyhp_cookie_str,                       /* tp_repr */
    0,                       /* tp_as_number */
    0,                       /* tp_as_sequence */
    0,                       /* tp_as_mapping */
    0,                       /* tp_hash */
    0,                       /* tp_call */
    (reprfunc)pyhp_cookie_str,                       /* tp_str */
    0,                       /* tp_getattro */
    0,                       /* tp_setattro */
    0,                       /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,   /* tp_flags */
    "PyHP Cookie",                       /* tp_doc */
    0,                       /* tp_traverse */
    0,                       /* tp_clear */
    0,                       /* tp_richcompare */
    0,                       /* tp_weaklistoffset */
    0,                       /* tp_iter */
    0,                       /* tp_iternext */
    PyHPCookie_methods,          /* tp_methods */
    PyHPCookie_members,                       /* tp_members */
    0,                       /* tp_getset */
    0,                       /* tp_base */
    0,                       /* tp_dict */
    0,                       /* tp_descr_get */
    0,                       /* tp_descr_set */
    0,                       /* tp_dictoffset */
    0,   /* tp_init */
    0,                       /* tp_alloc */
    (newfunc)PyHPCookie_new,                       /* tp_new */
};

/**
 * Unmarshalls the given data and creates a Cookie instance
 * from it.
 *
 * @param name The name of the cookie
 * @param content the content of the coockie, must be unpickable.
 *
 * @return a Cookie instance
 */
static PyObject *cookie_unpack(PyHPData *pydata, request_rec *r, char *name, char *content)
{
    pyhp_config * conf = pyhp_get_config(r);
    PyHPCookie *cookie = PyHPCookie_new(&PyHPCookieType, NULL, NULL);
    PyObject *result = Py_None;

    Py_XDECREF(cookie->name);
    cookie->name = PyString_FromString(name);

    if (strcmp(conf->session_name, name) == 0) {
        PyObject_SetAttrString((PyObject *)cookie, "data", PyString_FromString(content));
        result = (PyObject *)cookie;
    }
    else {
        PyObject *data;

        if (strncmp(PYHP_COOKIE_PREAMBLE, content, PYHP_COOKIE_PREAMBLE_LEN) == 0)
            data = pyhp_layer_unpack_object(pydata, content+PYHP_COOKIE_PREAMBLE_LEN);
        else {
            pyhp_log(r, PyHP_DEBUG, "Found non PyHP Cookie, treating as a string");
            data = PyString_FromString(content);
        }

        if (!data)
            result = Py_None;
        else {
            PyObject_SetAttrString((PyObject *)cookie, "data", data);
            result = (PyObject *)cookie;
        }
    }

    Py_INCREF(result);
    return result;
}

/**
 * Will parse the incoming headers of the given request to
 * create the cookies.in dictionary for the python layer
 * and the corrispective instances for every cookie found.
 */
void pyhp_cookies_parse(PyHPData *pydata, request_rec *r)
{
    PyObject *cm = pyhp_layer_get_object(pydata, "cookies");
    PyObject *dict = NULL;
    PyObject *cookie = NULL;
    const char *pair;
    const char *data = apr_table_get(r->headers_in, "Cookie");

    dict = PyModule_GetDict(cm);
    dict = PyDict_GetItemString(dict, "in");

    if (!dict)
        return;

    PyDict_Clear(dict);

    if(!data)
        return;

    while(*data && (pair = ap_getword(r->pool, &data, ';'))) {
        char *name, *value;
        if(*data == ' ') ++data;
        name = ap_getword(r->pool, &pair, '=');
        while(*pair && (value = ap_getword(r->pool, &pair, '&')))
            ap_unescape_url((char *)value);

        // Ignore or check for Version?
        if(!apr_strnatcasecmp(name, "$Version"))
            continue;

        // If we got a new cookie
        if(*name != '$') {
            //pyhp_logf(r, PyHP_DEBUG, "Cookie: %s\n", name);
            cookie = cookie_unpack(pydata, r, name, value);
            PyDict_SetItemString(dict, name, cookie);
            Py_XDECREF(cookie);
        }
    }
}

/**
 * Will read the cookies.out dictionary of the Python layer
 * and add all the found cookies to the output headers of the
 * given request.
 */
void pyhp_cookies_write(PyHPData * pydata, request_rec *r)
{
    PyObject *dict;
    PyObject *key, *value;
    Py_ssize_t pos = 0;

    //Write the cookies
    dict = pyhp_layer_get_object(pydata, "cookies");
    dict = PyModule_GetDict(dict);
    dict = PyDict_GetItemString(dict, "out");

    if (!dict)
        return;

    if (PyDict_Size(dict)) {
        while(PyDict_Next(dict, &pos, &key, &value)) {
            char *cookie_string = "";
            char *cookie_name = PyString_AsString(key);
            char *cookie_dump = NULL;

            PyObject *cookie = value;
            PyObject *cookie_data = PyObject_GetAttrString(cookie, "data");
            PyObject *cookie_attrs = ((PyHPCookie*)cookie)->attrs;

            PyObject *attr_key;
            PyObject *attr_val;
            Py_ssize_t attr_pos = 0;


            if ( ((PyHPCookie*)cookie)->cookie_type == PYHP_COOKIE_NORMAL )
                cookie_dump = apr_pstrcat(r->pool, PYHP_COOKIE_PREAMBLE, pyhp_layer_pack_object(pydata, cookie_data), NULL);
            else if ( ((PyHPCookie*)cookie)->cookie_type == PYHP_COOKIE_SESSION )
                cookie_dump = PyString_AsString(cookie_data);
            else
                pyhp_logf(r, PyHP_ERR, "Unknown cookie type %d for cookie '%s'", ((PyHPCookie*)cookie)->cookie_type,  cookie_name);

            cookie_string = apr_pstrcat(r->pool, cookie_string,
                                        cookie_name, "=",
                                        ap_escape_uri(r->pool, cookie_dump),
                                        "; Version=1; ",
                                        NULL);

            while(PyDict_Next(cookie_attrs, &attr_pos, &attr_key, &attr_val)) {
                //pyhp_logf(r, PyHP_DEBUG, "CATTR: %s", PyString_AsString(attr_key));
                cookie_string = apr_pstrcat(r->pool, cookie_string,
                                         PyString_AsString(attr_key), "=",
                                         PyString_AsString(attr_val),
                                         ";",
                                         NULL);
            }

            //pyhp_logf(r, PyHP_DEBUG, "WRITING COOKIE: %s", cookie_string);
            apr_table_add(r->headers_out, "Set-Cookie", cookie_string);
        }
    }

    //Clear dictionary of cookies to write
    PyDict_Clear(dict);

    //Clear dictionary of default attributes for cookies
    dict = pyhp_layer_get_object(pydata, "cookies");
    dict = PyModule_GetDict(dict);
    dict = PyDict_GetItemString(dict, "defaults");
    PyDict_Clear(dict);
}

/**
 * Python method for the Cookie Manager (pyhp.cookies module) to
 * create a new cookie and add it to the cookies output dictionary
 */
static PyObject *pyhp_cookie_create(PyObject *self, PyObject *args)
{
    PyObject *cookie_name = NULL;

    if (!PyArg_ParseTuple(args, "O", &cookie_name))
    	return NULL;

    if(!cookie_name) {
        Py_INCREF(Py_None);
        return Py_None;
    }

    PyObject *cout = PyDict_GetItemString(self, "out");
    PyHPCookie *cookie = PyHPCookie_new(&PyHPCookieType, NULL, NULL);
    Py_XDECREF(cookie->name);
    Py_INCREF(cookie_name);
    cookie->name = cookie_name;
    PyDict_SetItem(cout, cookie_name, (PyObject*)cookie);

    PyObject *defaults = PyDict_GetItemString(self, "defaults");
    PyObject *attr_key;
    PyObject *attr_val;
    Py_ssize_t attr_pos = 0;

    while(PyDict_Next(defaults, &attr_pos, &attr_key, &attr_val)) {
        PyDict_SetItem(cookie->attrs, attr_key, attr_val);
    }

    return (PyObject*)cookie;
}

/**
 * Python method for the Cookie Manager to retrieved the received cookie
 * from the input headers.
 */
static PyObject *pyhp_cookie_get(PyObject *self, PyObject *args)
{
    PyObject *cookie_name = NULL;

    if (!PyArg_ParseTuple(args, "O", &cookie_name))
    	return NULL;

    if(!cookie_name) {
        Py_INCREF(Py_None);
        return Py_None;
    }

    PyObject *cout = PyDict_GetItemString(self, "in");
    PyObject *cookie = PyDict_GetItem(cout, cookie_name);
    if (!cookie)
        cookie = Py_None;

    Py_INCREF(cookie);
    return cookie;
}

/**
 * Python method for the Cookie Manager to drop a cookie.
 * It will create a cookie with the same name of the specified one and
 * with expiration date 01-01-1970. The method will also return the created
 * cookie.
 * This should remove the preexisting cookie,
 * if the preexisting cookie had other attributes they must be set by hand
 * or some browsers would create a duplicate instead of removing the old one.
 */
static PyObject *pyhp_cookie_drop(PyObject *self, PyObject *args)
{
    PyObject *cookie_name = NULL;

    if (!PyArg_ParseTuple(args, "O", &cookie_name))
    	return NULL;

    if(!cookie_name) {
        Py_INCREF(Py_None);
        return Py_None;
    }

    PyObject *cout = PyDict_GetItemString(self, "out");
    PyHPCookie *cookie = PyHPCookie_new(&PyHPCookieType, NULL, NULL);
    Py_XDECREF(cookie->name);
    Py_INCREF(cookie_name);
    cookie->name = cookie_name;

    PyDict_SetItemString(cookie->attrs, "expires", PyString_FromString("Thu, 01-Jan-1970 00:00:00 GMT"));
    PyDict_SetItem(cout, cookie_name, (PyObject*)cookie);

    return (PyObject*)cookie;
}

/**
 * Creates a cookie for the current session id to store or remove it.
 *
 * @param session_id The session id as a python object
 * @param r The request for which to create the session
 */
void pyhp_cookies_create_session_cookie(PyHPData * pydata, PyObject * session_id, request_rec *r)
{
    pyhp_config * conf = pyhp_get_config(r);

    PyObject *cookies_module = pyhp_layer_get_object(pydata, "cookies");
    PyObject *cout = PyDict_GetItemString(PyModule_GetDict(cookies_module), "out");

    PyHPCookie *cookie = PyHPCookie_new(&PyHPCookieType, NULL, NULL);
    cookie->cookie_type = PYHP_COOKIE_SESSION;

    Py_XDECREF(cookie->name);
    cookie->name = PyString_FromString(conf->session_name);

    PyObject_SetAttrString((PyObject*)cookie, "data", session_id);
    //PyDict_SetItemString(cookie->dict, "id", session_id);
    PyDict_SetItemString(cout, "session", (PyObject*)cookie);
}

/**
 * Retrieves the currently available cookie with the stored session id
 */
PyObject * pyhp_cookies_get_session_cookie(PyHPData * pydata, request_rec *r)
{
    pyhp_config * conf = pyhp_get_config(r);
    PyObject *session_id = NULL;
    PyObject *cookies_module = pyhp_layer_get_object(pydata, "cookies");
    PyObject *cin = PyDict_GetItemString(PyModule_GetDict(cookies_module), "in");

    PyObject *session_cookie = PyDict_GetItemString(cin, conf->session_name);
    if (session_cookie) {
        /*PyObject *cookie_data = PyObject_GetAttrString(session_cookie, "data");*/
        session_id = PyObject_GetAttrString(session_cookie, "data"); //PyDict_GetItemString(cookie_data, "id");
    }

    return session_id;
}

static PyMethodDef cm_method_table[] = {
    { "create", (PyCFunction)pyhp_cookie_create, METH_VARARGS,
                                    "Create a new cookie instance" },
    { "get", (PyCFunction)pyhp_cookie_get, METH_VARARGS,
                                    "Retrieves a received cookie" },
    { "drop", (PyCFunction)pyhp_cookie_drop, METH_VARARGS,
                                    "Deletes a cookie" },
    { NULL, NULL }
};

/**
 * Exports the logging constants and methods to the python layer
 * @see pyhp_layer_init
 */
void pyhp_cookies_export(PyObject *dict)
{
    PyObject *tmp;
    PyObject *self = PyDict_New();
    PyObject *cm = Py_InitModule4("pyhp_cookie_manager", cm_method_table, "",
                                  self, PYTHON_API_VERSION);
    PyDict_SetItemString(dict, "cookies", cm);
    Py_XDECREF(cm);

    if (PyType_Ready(&PyHPCookieType) < 0)
        return;

    Py_INCREF(&PyHPCookieType);
    PyModule_AddObject(cm, "Cookie", (PyObject *) &PyHPCookieType);

    dict = PyModule_GetDict(cm);

    tmp = PyDict_New();
    PyDict_SetItemString(self, "in", tmp);
    PyDict_SetItemString(dict, "in", tmp);
    Py_XDECREF(tmp);

    tmp = PyDict_New();
    PyDict_SetItemString(self, "out", tmp);
    PyDict_SetItemString(dict, "out", tmp);
    Py_XDECREF(tmp);

    tmp = PyDict_New();
    PyDict_SetItemString(self, "defaults", tmp);
    PyDict_SetItemString(dict, "defaults", tmp);
    Py_XDECREF(tmp);
}
